/*
 * Copyright 2017 - 2024 the original author or authors.
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see [https://www.gnu.org/licenses/]
 */

package infra.bytecode.core;

import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.List;

import infra.bytecode.ClassWriter;
import infra.bytecode.MethodVisitor;
import infra.bytecode.Opcodes;
import infra.expression.spel.SpelNode;
import infra.lang.Assert;
import infra.lang.Nullable;
import infra.util.CollectionUtils;

/**
 * Manages the class being generated by the compilation process.
 *
 * <p>Records intermediate compilation state as the bytecode is generated.
 * Also includes various bytecode generation helper functions.
 *
 * @author Andy Clement
 * @author Juergen Hoeller
 * @author <a href="https://github.com/TAKETODAY">Harry Yang</a>
 * @since 4.0
 */
public class CodeFlow implements Opcodes {

  /**
   * Name of the class being generated. Typically used when generating code
   * that accesses freshly generated fields on the generated type.
   */
  private final String className;

  /**
   * The current class being generated.
   */
  private final ClassWriter classWriter;

  /**
   * Record the type of what is on top of the bytecode stack (i.e. the type of the
   * output from the previous expression component). New scopes are used to evaluate
   * sub-expressions like the expressions for the argument values in a method invocation
   * expression.
   */
  private final Deque<List<String>> compilationScopes;

  /**
   * As SpEL AST nodes are called to generate code for the main evaluation method
   * they can register to add a field to this class. Any registered FieldAdders
   * will be called after the main evaluation function has finished being generated.
   */
  @Nullable
  private List<FieldAdder> fieldAdders;

  /**
   * As SpEL AST nodes are called to generate code for the main evaluation method
   * they can register to add code to a static initializer in the class. Any
   * registered ClinitAdders will be called after the main evaluation function
   * has finished being generated.
   */
  @Nullable
  private List<ClinitAdder> clinitAdders;

  /**
   * When code generation requires holding a value in a class level field, this
   * is used to track the next available field id (used as a name suffix).
   */
  private int nextFieldId = 1;

  /**
   * When code generation requires an intermediate variable within a method,
   * this method records the next available variable (variable 0 is 'this').
   */
  private int nextFreeVariableId = 1;

  /**
   * Construct a new {@code CodeFlow} for the given class.
   *
   * @param className the name of the class
   * @param classWriter the corresponding ASM {@code ClassWriter}
   */
  public CodeFlow(String className, ClassWriter classWriter) {
    this.className = className;
    this.classWriter = classWriter;
    this.compilationScopes = new ArrayDeque<>();
    this.compilationScopes.add(new ArrayList<>());
  }

  /**
   * Push the byte code to load the target (i.e. what was passed as the first argument
   * to CompiledExpression.getValue(target, context))
   *
   * @param mv the method visitor into which the load instruction should be inserted
   */
  public void loadTarget(MethodVisitor mv) {
    mv.visitVarInsn(ALOAD, 1);
  }

  /**
   * Push the bytecode to load the EvaluationContext (the second parameter passed to
   * the compiled expression method).
   *
   * @param mv the method visitor into which the load instruction should be inserted
   */
  public void loadEvaluationContext(MethodVisitor mv) {
    mv.visitVarInsn(ALOAD, 2);
  }

  /**
   * Record the descriptor for the most recently evaluated expression element.
   *
   * @param descriptor type descriptor for most recently evaluated element
   */
  public void pushDescriptor(@Nullable String descriptor) {
    if (descriptor != null) {
      this.compilationScopes.element().add(descriptor);
    }
  }

  /**
   * Enter a new compilation scope, usually due to nested expression evaluation. For
   * example when the arguments for a method invocation expression are being evaluated,
   * each argument will be evaluated in a new scope.
   */
  public void enterCompilationScope() {
    this.compilationScopes.push(new ArrayList<>());
  }

  /**
   * Exit a compilation scope, usually after a nested expression has been evaluated. For
   * example after an argument for a method invocation has been evaluated this method
   * returns us to the previous (outer) scope.
   */
  public void exitCompilationScope() {
    this.compilationScopes.pop();
  }

  /**
   * Return the descriptor for the item currently on top of the stack (in the current scope).
   */
  @Nullable
  public String lastDescriptor() {
    return CollectionUtils.lastElement(this.compilationScopes.peek());
  }

  /**
   * If the codeflow shows the last expression evaluated to java.lang.Boolean then
   * insert the necessary instructions to unbox that to a boolean primitive.
   *
   * @param mv the method visitor into which new instructions should be inserted
   */
  public void unboxBooleanIfNecessary(MethodVisitor mv) {
    if ("Ljava/lang/Boolean".equals(lastDescriptor())) {
      mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Boolean", "booleanValue", "()Z", false);
    }
  }

  /**
   * Called after the main expression evaluation method has been generated, this
   * method will call back any registered FieldAdders or ClinitAdders to add any
   * extra information to the class representing the compiled expression.
   */
  public void finish() {
    if (this.fieldAdders != null) {
      for (FieldAdder fieldAdder : this.fieldAdders) {
        fieldAdder.generateField(this.classWriter, this);
      }
    }
    if (this.clinitAdders != null) {
      MethodVisitor mv = this.classWriter.visitMethod(ACC_PUBLIC | ACC_STATIC, "<clinit>", "()V", null, null);
      mv.visitCode();
      this.nextFreeVariableId = 0;  // to 0 because there is no 'this' in a clinit
      for (ClinitAdder clinitAdder : this.clinitAdders) {
        clinitAdder.generateCode(mv, this);
      }
      mv.visitInsn(RETURN);
      mv.visitMaxs(0, 0);  // not supplied due to COMPUTE_MAXS
      mv.visitEnd();
    }
  }

  /**
   * Register a FieldAdder which will add a new field to the generated
   * class to support the code produced by an AST node's primary
   * generateCode() method.
   */
  public void registerNewField(FieldAdder fieldAdder) {
    if (this.fieldAdders == null) {
      this.fieldAdders = new ArrayList<>();
    }
    this.fieldAdders.add(fieldAdder);
  }

  /**
   * Register a ClinitAdder which will add code to the static
   * initializer in the generated class to support the code
   * produced by an AST node's primary generateCode() method.
   */
  public void registerNewClinit(ClinitAdder clinitAdder) {
    if (this.clinitAdders == null) {
      this.clinitAdders = new ArrayList<>();
    }
    this.clinitAdders.add(clinitAdder);
  }

  public int nextFieldId() {
    return this.nextFieldId++;
  }

  public int nextFreeVariableId() {
    return this.nextFreeVariableId++;
  }

  public String getClassName() {
    return this.className;
  }

  /**
   * Generate bytecode that loads the supplied argument onto the stack.
   * <p>Delegates to {@link #generateCodeForArgument(MethodVisitor, SpelNode, String)}
   * with the {@linkplain #toDescriptor(Class) descriptor} for
   * the supplied {@code requiredType}.
   * <p>This method also performs any boxing, unboxing, or check-casting
   * necessary to ensure that the type of the argument on the stack matches the
   * supplied {@code requiredType}.
   * <p>Use this method when a node in the AST will be used as an argument for
   * a constructor or method invocation. For example, if you wish to invoke a
   * method with an {@code indexNode} that must be of type {@code int} for the
   * actual method invocation within bytecode, you would call
   * {@code codeFlow.generateCodeForArgument(methodVisitor, indexNode, int.class)}.
   *
   * @param methodVisitor the ASM {@link MethodVisitor} into which code should
   * be generated
   * @param argument a {@link SpelNode} that represents an argument to a method
   * or constructor
   * @param requiredType the required type for the argument when invoking the
   * corresponding constructor or method
   * @see #generateCodeForArgument(MethodVisitor, SpelNode, String)
   * @since 5.0
   */
  public void generateCodeForArgument(MethodVisitor methodVisitor, SpelNode argument, Class<?> requiredType) {
    generateCodeForArgument(methodVisitor, argument, toDescriptor(requiredType));
  }

  /**
   * Generate bytecode that loads the supplied argument onto the stack.
   * <p>This method also performs any boxing, unboxing, or check-casting
   * necessary to ensure that the type of the argument on the stack matches the
   * supplied {@code requiredTypeDesc}.
   * <p>Use this method when a node in the AST will be used as an argument for
   * a constructor or method invocation. For example, if you wish to invoke a
   * method with an {@code indexNode} that must be of type {@code int} for the
   * actual method invocation within bytecode, you would call
   * {@code codeFlow.generateCodeForArgument(methodVisitor, indexNode, "I")}.
   *
   * @param methodVisitor the ASM {@link MethodVisitor} into which code should
   * be generated
   * @param argument a {@link SpelNode} that represents an argument to a method
   * or constructor
   * @param requiredTypeDesc a descriptor for the required type for the argument
   * when invoking the corresponding constructor or method
   * @see #generateCodeForArgument(MethodVisitor, SpelNode, Class)
   * @see #toDescriptor(Class)
   * @since 5.0
   */
  public void generateCodeForArgument(MethodVisitor methodVisitor, SpelNode argument, String requiredTypeDesc) {
    enterCompilationScope();
    argument.generateCode(methodVisitor, this);
    String lastDesc = lastDescriptor();
    Assert.state(lastDesc != null, "No last descriptor");
    boolean primitiveOnStack = isPrimitive(lastDesc);
    // Check if we need to box it.
    if (primitiveOnStack && requiredTypeDesc.charAt(0) == 'L') {
      insertBoxIfNecessary(methodVisitor, lastDesc.charAt(0));
    }
    // Check if we need to unbox it.
    else if (requiredTypeDesc.length() == 1 && !primitiveOnStack) {
      insertUnboxInsns(methodVisitor, requiredTypeDesc.charAt(0), lastDesc);
    }
    // Check if we need to check-cast
    else if (!requiredTypeDesc.equals(lastDesc)) {
      // This would be unnecessary in the case of subtyping (e.g. method takes Number but Integer passed in)
      insertCheckCast(methodVisitor, requiredTypeDesc);
    }
    exitCompilationScope();
  }

  /**
   * Insert any necessary cast and value call to convert from a boxed type to a
   * primitive value.
   *
   * @param mv the method visitor into which instructions should be inserted
   * @param ch the primitive type desired as output
   * @param stackDescriptor the descriptor of the type on top of the stack
   */
  public static void insertUnboxInsns(MethodVisitor mv, char ch, @Nullable String stackDescriptor) {
    if (stackDescriptor == null) {
      return;
    }
    switch (ch) {
      case 'Z' -> {
        if (!stackDescriptor.equals("Ljava/lang/Boolean")) {
          mv.visitTypeInsn(CHECKCAST, "java/lang/Boolean");
        }
        mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Boolean", "booleanValue", "()Z", false);
      }
      case 'B' -> {
        if (!stackDescriptor.equals("Ljava/lang/Byte")) {
          mv.visitTypeInsn(CHECKCAST, "java/lang/Byte");
        }
        mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Byte", "byteValue", "()B", false);
      }
      case 'C' -> {
        if (!stackDescriptor.equals("Ljava/lang/Character")) {
          mv.visitTypeInsn(CHECKCAST, "java/lang/Character");
        }
        mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Character", "charValue", "()C", false);
      }
      case 'D' -> {
        if (!stackDescriptor.equals("Ljava/lang/Double")) {
          mv.visitTypeInsn(CHECKCAST, "java/lang/Double");
        }
        mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Double", "doubleValue", "()D", false);
      }
      case 'F' -> {
        if (!stackDescriptor.equals("Ljava/lang/Float")) {
          mv.visitTypeInsn(CHECKCAST, "java/lang/Float");
        }
        mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Float", "floatValue", "()F", false);
      }
      case 'I' -> {
        if (!stackDescriptor.equals("Ljava/lang/Integer")) {
          mv.visitTypeInsn(CHECKCAST, "java/lang/Integer");
        }
        mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Integer", "intValue", "()I", false);
      }
      case 'J' -> {
        if (!stackDescriptor.equals("Ljava/lang/Long")) {
          mv.visitTypeInsn(CHECKCAST, "java/lang/Long");
        }
        mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Long", "longValue", "()J", false);
      }
      case 'S' -> {
        if (!stackDescriptor.equals("Ljava/lang/Short")) {
          mv.visitTypeInsn(CHECKCAST, "java/lang/Short");
        }
        mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Short", "shortValue", "()S", false);
      }
      default -> throw new IllegalArgumentException("Unboxing should not be attempted for descriptor '" + ch + "'");
    }
  }

  /**
   * For numbers, use the appropriate method on the number to convert it to the primitive type requested.
   *
   * @param mv the method visitor into which instructions should be inserted
   * @param targetDescriptor the primitive type desired as output
   * @param stackDescriptor the descriptor of the type on top of the stack
   */
  public static void insertUnboxNumberInsns(MethodVisitor mv, char targetDescriptor, @Nullable String stackDescriptor) {
    if (stackDescriptor == null) {
      return;
    }

    switch (targetDescriptor) {
      case 'D' -> {
        if (stackDescriptor.equals("Ljava/lang/Object")) {
          mv.visitTypeInsn(CHECKCAST, "java/lang/Number");
        }
        mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Number", "doubleValue", "()D", false);
      }
      case 'F' -> {
        if (stackDescriptor.equals("Ljava/lang/Object")) {
          mv.visitTypeInsn(CHECKCAST, "java/lang/Number");
        }
        mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Number", "floatValue", "()F", false);
      }
      case 'J' -> {
        if (stackDescriptor.equals("Ljava/lang/Object")) {
          mv.visitTypeInsn(CHECKCAST, "java/lang/Number");
        }
        mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Number", "longValue", "()J", false);
      }
      case 'I' -> {
        if (stackDescriptor.equals("Ljava/lang/Object")) {
          mv.visitTypeInsn(CHECKCAST, "java/lang/Number");
        }
        mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Number", "intValue", "()I", false);
      }
      // does not handle Z, B, C, S
      default -> throw new IllegalArgumentException("Unboxing should not be attempted for descriptor '" + targetDescriptor + "'");
    }
  }

  /**
   * Insert any necessary numeric conversion bytecodes based upon what is on the
   * stack and the desired target type.
   *
   * @param mv the method visitor into which instructions should be placed
   * @param targetDescriptor the (primitive) descriptor of the target type
   * @param stackDescriptor the descriptor of the operand on top of the stack
   */
  public static void insertAnyNecessaryTypeConversionBytecodes(MethodVisitor mv, char targetDescriptor, String stackDescriptor) {
    if (!CodeFlow.isPrimitive(stackDescriptor)) {
      return;
    }
    char stackTop = stackDescriptor.charAt(0);
    switch (stackTop) {
      case 'I', 'B', 'S', 'C' -> {
        switch (targetDescriptor) {
          case 'D' -> mv.visitInsn(I2D);
          case 'F' -> mv.visitInsn(I2F);
          case 'J' -> mv.visitInsn(I2L);
          case 'I' -> { /* no-op */ }
          default -> throw new IllegalStateException("Cannot get from " + stackTop + " to " + targetDescriptor);
        }
      }
      case 'J' -> {
        switch (targetDescriptor) {
          case 'D' -> mv.visitInsn(L2D);
          case 'F' -> mv.visitInsn(L2F);
          case 'J' -> { /* no-op */ }
          case 'I' -> mv.visitInsn(L2I);
          default -> throw new IllegalStateException("Cannot get from " + stackTop + " to " + targetDescriptor);
        }
      }
      case 'F' -> {
        switch (targetDescriptor) {
          case 'D' -> mv.visitInsn(F2D);
          case 'F' -> { /* no-op */ }
          case 'J' -> mv.visitInsn(F2L);
          case 'I' -> mv.visitInsn(F2I);
          default -> throw new IllegalStateException("Cannot get from " + stackTop + " to " + targetDescriptor);
        }
      }
      case 'D' -> {
        switch (targetDescriptor) {
          case 'D' -> { /* no-op */ }
          case 'F' -> mv.visitInsn(D2F);
          case 'J' -> mv.visitInsn(D2L);
          case 'I' -> mv.visitInsn(D2I);
          default -> throw new IllegalStateException("Cannot get from " + stackDescriptor + " to " + targetDescriptor);
        }
      }
    }
  }

  /**
   * Create the JVM signature descriptor for a method. This consists of the descriptors
   * for the method parameters surrounded with parentheses, followed by the
   * descriptor for the return type. Note the descriptors here are JVM descriptors,
   * unlike the other descriptor forms the compiler is using which do not include the
   * trailing semicolon.
   *
   * @param method the method
   * @return a String signature descriptor (e.g. "(ILjava/lang/String;)V")
   */
  public static String createSignatureDescriptor(Method method) {
    Class<?>[] params = method.getParameterTypes();
    StringBuilder sb = new StringBuilder();
    sb.append('(');
    for (Class<?> param : params) {
      sb.append(toJvmDescriptor(param));
    }
    sb.append(')');
    sb.append(toJvmDescriptor(method.getReturnType()));
    return sb.toString();
  }

  /**
   * Create the JVM signature descriptor for a constructor. This consists of the
   * descriptors for the constructor parameters surrounded with parentheses, followed by
   * the descriptor for the return type, which is always "V". Note the
   * descriptors here are JVM descriptors, unlike the other descriptor forms the
   * compiler is using which do not include the trailing semicolon.
   *
   * @param ctor the constructor
   * @return a String signature descriptor (e.g. "(ILjava/lang/String;)V")
   */
  public static String createSignatureDescriptor(Constructor<?> ctor) {
    Class<?>[] params = ctor.getParameterTypes();
    StringBuilder sb = new StringBuilder();
    sb.append('(');
    for (Class<?> param : params) {
      sb.append(toJvmDescriptor(param));
    }
    sb.append(")V");
    return sb.toString();
  }

  /**
   * Determine the JVM descriptor for a specified class. Unlike the other descriptors
   * used in the compilation process, this is the one the JVM wants, so this one
   * includes any necessary trailing semicolon (e.g. Ljava/lang/String; rather than
   * Ljava/lang/String)
   *
   * @param clazz a class
   * @return the JVM descriptor for the class
   */
  public static String toJvmDescriptor(Class<?> clazz) {
    StringBuilder sb = new StringBuilder();
    if (clazz.isArray()) {
      while (clazz.isArray()) {
        sb.append('[');
        clazz = clazz.componentType();
      }
    }
    if (clazz.isPrimitive()) {
      if (clazz == boolean.class) {
        sb.append('Z');
      }
      else if (clazz == byte.class) {
        sb.append('B');
      }
      else if (clazz == char.class) {
        sb.append('C');
      }
      else if (clazz == double.class) {
        sb.append('D');
      }
      else if (clazz == float.class) {
        sb.append('F');
      }
      else if (clazz == int.class) {
        sb.append('I');
      }
      else if (clazz == long.class) {
        sb.append('J');
      }
      else if (clazz == short.class) {
        sb.append('S');
      }
      else if (clazz == void.class) {
        sb.append('V');
      }
    }
    else {
      sb.append('L');
      sb.append(clazz.getName().replace('.', '/'));
      sb.append(';');
    }
    return sb.toString();
  }

  /**
   * Determine the descriptor for an object instance (or {@code null}).
   *
   * @param value an object (possibly {@code null})
   * @return the type descriptor for the object
   * (descriptor is "Ljava/lang/Object" for {@code null} value)
   */
  public static String toDescriptorFromObject(@Nullable Object value) {
    if (value == null) {
      return "Ljava/lang/Object";
    }
    else {
      return toDescriptor(value.getClass());
    }
  }

  /**
   * Determine whether the descriptor is for a boolean primitive or boolean reference type.
   *
   * @param descriptor type descriptor
   * @return {@code true} if the descriptor is boolean compatible
   */
  public static boolean isBooleanCompatible(@Nullable String descriptor) {
    return (descriptor != null && (descriptor.equals("Z") || descriptor.equals("Ljava/lang/Boolean")));
  }

  /**
   * Determine whether the descriptor is for a primitive type or {@code void}.
   *
   * @param descriptor type descriptor
   * @return {@code true} if a primitive type or {@code void}
   */
  public static boolean isPrimitive(@Nullable String descriptor) {
    return (descriptor != null && descriptor.length() == 1);
  }

  /**
   * Determine whether the descriptor is for a primitive array (e.g. "[[I").
   *
   * @param descriptor the descriptor for a possible primitive array
   * @return {@code true} if the descriptor a primitive array
   */
  public static boolean isPrimitiveArray(@Nullable String descriptor) {
    if (descriptor == null) {
      return false;
    }
    boolean primitive = true;
    for (int i = 0, max = descriptor.length(); i < max; i++) {
      char ch = descriptor.charAt(i);
      if (ch == '[') {
        continue;
      }
      primitive = (ch != 'L');
      break;
    }
    return primitive;
  }

  /**
   * Determine whether boxing/unboxing can get from one type to the other.
   * <p>Assumes at least one of the types is in boxed form (i.e. single char descriptor).
   *
   * @return {@code true} if it is possible to get (via boxing) from one descriptor to the other
   */
  public static boolean areBoxingCompatible(String desc1, String desc2) {
    if (desc1.equals(desc2)) {
      return true;
    }
    if (desc1.length() == 1) {
      return checkPairs(desc1, desc2);
    }
    else if (desc2.length() == 1) {
      return checkPairs(desc2, desc1);
    }
    return false;
  }

  private static boolean checkPairs(String desc1, String desc2) {
    return switch (desc1) {
      case "Z" -> desc2.equals("Ljava/lang/Boolean");
      case "D" -> desc2.equals("Ljava/lang/Double");
      case "F" -> desc2.equals("Ljava/lang/Float");
      case "I" -> desc2.equals("Ljava/lang/Integer");
      case "J" -> desc2.equals("Ljava/lang/Long");
      default -> false;
    };
  }

  /**
   * Determine if the supplied descriptor is for a supported number type or boolean. The
   * compilation process only (currently) supports certain number types. These are
   * double, float, long and int.
   *
   * @param descriptor the descriptor for a type
   * @return {@code true} if the descriptor is for a supported numeric type or boolean
   */
  public static boolean isPrimitiveOrUnboxableSupportedNumberOrBoolean(@Nullable String descriptor) {
    if (descriptor == null) {
      return false;
    }
    if (isPrimitiveOrUnboxableSupportedNumber(descriptor)) {
      return true;
    }
    return ("Z".equals(descriptor) || descriptor.equals("Ljava/lang/Boolean"));
  }

  /**
   * Determine if the supplied descriptor is for a supported number. The compilation
   * process only (currently) supports certain number types. These are double, float,
   * long and int.
   *
   * @param descriptor the descriptor for a type
   * @return {@code true} if the descriptor is for a supported numeric type
   */
  public static boolean isPrimitiveOrUnboxableSupportedNumber(@Nullable String descriptor) {
    if (descriptor == null) {
      return false;
    }
    if (descriptor.length() == 1) {
      return "DFIJ".contains(descriptor);
    }
    if (descriptor.startsWith("Ljava/lang/")) {
      String name = descriptor.substring("Ljava/lang/".length());
      return (name.equals("Double") || name.equals("Float") || name.equals("Integer") || name.equals("Long"));
    }
    return false;
  }

  /**
   * Determine whether the given number is to be considered as an integer
   * for the purposes of a numeric operation at the bytecode level.
   *
   * @param number the number to check
   * @return {@code true} if it is an {@link Integer}, {@link Short} or {@link Byte}
   */
  public static boolean isIntegerForNumericOp(Number number) {
    return (number instanceof Integer || number instanceof Short || number instanceof Byte);
  }

  /**
   * Convert a type descriptor to the single character primitive descriptor.
   *
   * @param descriptor a descriptor for a type that should have a primitive representation
   * @return the single character descriptor for a primitive input descriptor
   */
  public static char toPrimitiveTargetDesc(String descriptor) {
    if (descriptor.length() == 1) {
      return descriptor.charAt(0);
    }
    return switch (descriptor) {
      case "Ljava/lang/Double" -> 'D';
      case "Ljava/lang/Float" -> 'F';
      case "Ljava/lang/Integer" -> 'I';
      case "Ljava/lang/Long" -> 'J';
      case "Ljava/lang/Boolean" -> 'Z';
      case "Ljava/lang/Character" -> 'C';
      case "Ljava/lang/Byte" -> 'B';
      case "Ljava/lang/Short" -> 'S';
      default -> throw new IllegalStateException("No primitive for '" + descriptor + "'");
    };
  }

  /**
   * Insert the appropriate CHECKCAST instruction for the supplied descriptor.
   *
   * @param mv the method visitor into which the instruction should be inserted
   * @param descriptor the descriptor of the type to cast to
   */
  public static void insertCheckCast(MethodVisitor mv, @Nullable String descriptor) {
    if (descriptor != null && descriptor.length() != 1) {
      if (descriptor.charAt(0) == '[') {
        if (isPrimitiveArray(descriptor)) {
          mv.visitTypeInsn(CHECKCAST, descriptor);
        }
        else {
          mv.visitTypeInsn(CHECKCAST, descriptor + ";");
        }
      }
      else {
        if (!descriptor.equals("Ljava/lang/Object")) {
          // This is chopping off the 'L' to leave us with "java/lang/String"
          mv.visitTypeInsn(CHECKCAST, descriptor.substring(1));
        }
      }
    }
  }

  /**
   * Determine the appropriate boxing instruction for a specific type (if it needs
   * boxing) and insert the instruction into the supplied visitor.
   *
   * @param mv the method visitor for the new instructions
   * @param descriptor the descriptor of a type that may or may not need boxing
   */
  public static void insertBoxIfNecessary(MethodVisitor mv, @Nullable String descriptor) {
    if (descriptor != null && descriptor.length() == 1) {
      insertBoxIfNecessary(mv, descriptor.charAt(0));
    }
  }

  /**
   * Determine the appropriate boxing instruction for a specific type (if it needs
   * boxing) and insert the instruction into the supplied visitor.
   *
   * @param mv the method visitor for the new instructions
   * @param ch the descriptor of the type that might need boxing
   */
  public static void insertBoxIfNecessary(MethodVisitor mv, char ch) {
    switch (ch) {
      case 'Z' -> mv.visitMethodInsn(INVOKESTATIC, "java/lang/Boolean", "valueOf", "(Z)Ljava/lang/Boolean;", false);
      case 'B' -> mv.visitMethodInsn(INVOKESTATIC, "java/lang/Byte", "valueOf", "(B)Ljava/lang/Byte;", false);
      case 'C' -> mv.visitMethodInsn(INVOKESTATIC, "java/lang/Character", "valueOf", "(C)Ljava/lang/Character;", false);
      case 'D' -> mv.visitMethodInsn(INVOKESTATIC, "java/lang/Double", "valueOf", "(D)Ljava/lang/Double;", false);
      case 'F' -> mv.visitMethodInsn(INVOKESTATIC, "java/lang/Float", "valueOf", "(F)Ljava/lang/Float;", false);
      case 'I' -> mv.visitMethodInsn(INVOKESTATIC, "java/lang/Integer", "valueOf", "(I)Ljava/lang/Integer;", false);
      case 'J' -> mv.visitMethodInsn(INVOKESTATIC, "java/lang/Long", "valueOf", "(J)Ljava/lang/Long;", false);
      case 'S' -> mv.visitMethodInsn(INVOKESTATIC, "java/lang/Short", "valueOf", "(S)Ljava/lang/Short;", false);
      case 'L', 'V', '[' -> {
        // no box needed
      }
      default -> throw new IllegalArgumentException("Boxing should not be attempted for descriptor '" + ch + "'");
    }
  }

  /**
   * Deduce the descriptor for a type. Descriptors are like JVM type names but missing the
   * trailing ';' so for Object the descriptor is "Ljava/lang/Object" for int it is "I".
   *
   * @param type the type (may be primitive) for which to determine the descriptor
   * @return the descriptor
   */
  public static String toDescriptor(Class<?> type) {
    String name = type.getName();
    if (type.isPrimitive()) {
      switch (name.length()) {
        case 3:
          return "I";
        case 4:
          return switch (name) {
            case "byte" -> "B";
            case "char" -> "C";
            case "long" -> "J";
            case "void" -> "V";
            default -> throw new IllegalArgumentException("Unknown primitive type: " + name);
          };
        case 5:
          if (name.equals("float")) {
            return "F";
          }
          else if (name.equals("short")) {
            return "S";
          }
          break;
        case 6:
          if (name.equals("double")) {
            return "D";
          }
          break;
        case 7:
          if (name.equals("boolean")) {
            return "Z";
          }
          break;
        default:
          throw new IllegalArgumentException("Unknown primitive type: " + name);
      }
    }
    else {
      if (name.charAt(0) != '[') {
        return "L" + type.getName().replace('.', '/');
      }
      else {
        if (name.endsWith(";")) {
          return name.substring(0, name.length() - 1).replace('.', '/');
        }
        else {
          return name;  // array has primitive component type
        }
      }
    }
    return "";
  }

  /**
   * Create an array of descriptors representing the parameter types for the supplied
   * method. Returns a zero sized array if there are no parameters.
   *
   * @param method a Method
   * @return a String array of descriptors, one entry for each method parameter
   */
  public static String[] toParamDescriptors(Method method) {
    return toDescriptors(method.getParameterTypes());
  }

  /**
   * Create an array of descriptors representing the parameter types for the supplied
   * constructor. Returns a zero sized array if there are no parameters.
   *
   * @param ctor a Constructor
   * @return a String array of descriptors, one entry for each constructor parameter
   */
  public static String[] toParamDescriptors(Constructor<?> ctor) {
    return toDescriptors(ctor.getParameterTypes());
  }

  /**
   * Create an array of descriptors from an array of classes.
   *
   * @param types the input array of classes
   * @return an array of descriptors
   */
  public static String[] toDescriptors(Class<?>[] types) {
    int typesCount = types.length;
    String[] descriptors = new String[typesCount];
    for (int p = 0; p < typesCount; p++) {
      descriptors[p] = toDescriptor(types[p]);
    }
    return descriptors;
  }

  /**
   * Create the optimal instruction for loading a number on the stack.
   *
   * @param mv where to insert the bytecode
   * @param value the value to be loaded
   */
  public static void insertOptimalLoad(MethodVisitor mv, int value) {
    if (value < 6) {
      mv.visitInsn(ICONST_0 + value);
    }
    else if (value < Byte.MAX_VALUE) {
      mv.visitIntInsn(BIPUSH, value);
    }
    else if (value < Short.MAX_VALUE) {
      mv.visitIntInsn(SIPUSH, value);
    }
    else {
      mv.visitLdcInsn(value);
    }
  }

  /**
   * Produce appropriate bytecode to store a stack item in an array. The
   * instruction to use varies depending on whether the type
   * is a primitive or reference type.
   *
   * @param mv where to insert the bytecode
   * @param arrayElementType the type of the array elements
   */
  public static void insertArrayStore(MethodVisitor mv, String arrayElementType) {
    if (arrayElementType.length() == 1) {
      switch (arrayElementType.charAt(0)) {
        case 'B', 'Z' -> mv.visitInsn(BASTORE);
        case 'I' -> mv.visitInsn(IASTORE);
        case 'J' -> mv.visitInsn(LASTORE);
        case 'F' -> mv.visitInsn(FASTORE);
        case 'D' -> mv.visitInsn(DASTORE);
        case 'C' -> mv.visitInsn(CASTORE);
        case 'S' -> mv.visitInsn(SASTORE);
        default -> throw new IllegalArgumentException("Unexpected array type " + arrayElementType.charAt(0));
      }
    }
    else {
      mv.visitInsn(AASTORE);
    }
  }

  /**
   * Determine the appropriate T tag to use for the NEWARRAY bytecode.
   *
   * @param arrayType the array primitive component type
   * @return the T tag to use for NEWARRAY
   */
  public static int arrayCodeFor(String arrayType) {
    return switch (arrayType.charAt(0)) {
      case 'I' -> T_INT;
      case 'J' -> T_LONG;
      case 'F' -> T_FLOAT;
      case 'D' -> T_DOUBLE;
      case 'B' -> T_BYTE;
      case 'C' -> T_CHAR;
      case 'S' -> T_SHORT;
      case 'Z' -> T_BOOLEAN;
      default -> throw new IllegalArgumentException("Unexpected array type " + arrayType.charAt(0));
    };
  }

  /**
   * Return if the supplied array type has a core component reference type.
   */
  public static boolean isReferenceTypeArray(String arrayType) {
    int length = arrayType.length();
    for (int i = 0; i < length; i++) {
      char ch = arrayType.charAt(i);
      if (ch == '[') {
        continue;
      }
      return (ch == 'L');
    }
    return false;
  }

  /**
   * Produce the correct bytecode to build an array. The opcode to use and the
   * signature to pass along with the opcode can vary depending on the signature
   * of the array type.
   *
   * @param mv the method visitor into which code should be inserted
   * @param size the size of the array
   * @param arrayType the type of the array
   */
  public static void insertNewArrayCode(MethodVisitor mv, int size, String arrayType) {
    insertOptimalLoad(mv, size);
    if (arrayType.length() == 1) {
      mv.visitIntInsn(NEWARRAY, CodeFlow.arrayCodeFor(arrayType));
    }
    else {
      if (arrayType.charAt(0) == '[') {
        // Handling the nested array case here.
        // If vararg is [[I then we want [I and not [I;
        if (CodeFlow.isReferenceTypeArray(arrayType)) {
          mv.visitTypeInsn(ANEWARRAY, arrayType + ";");
        }
        else {
          mv.visitTypeInsn(ANEWARRAY, arrayType);
        }
      }
      else {
        mv.visitTypeInsn(ANEWARRAY, arrayType.substring(1));
      }
    }
  }

  /**
   * For use in mathematical operators, handles converting from a (possibly boxed)
   * number on the stack to a primitive numeric type.
   * <p>For example, from an Integer to a double, just need to call 'Number.doubleValue()'
   * but from an int to a double, need to use the bytecode 'i2d'.
   *
   * @param mv the method visitor when instructions should be appended
   * @param stackDescriptor a descriptor of the operand on the stack
   * @param targetDescriptor a primitive type descriptor
   */
  public static void insertNumericUnboxOrPrimitiveTypeCoercion(
          MethodVisitor mv, @Nullable String stackDescriptor, char targetDescriptor) {

    if (!CodeFlow.isPrimitive(stackDescriptor)) {
      CodeFlow.insertUnboxNumberInsns(mv, targetDescriptor, stackDescriptor);
    }
    else {
      CodeFlow.insertAnyNecessaryTypeConversionBytecodes(mv, targetDescriptor, stackDescriptor);
    }
  }

  public static String toBoxedDescriptor(String primitiveDescriptor) {
    return switch (primitiveDescriptor.charAt(0)) {
      case 'I' -> "Ljava/lang/Integer";
      case 'J' -> "Ljava/lang/Long";
      case 'F' -> "Ljava/lang/Float";
      case 'D' -> "Ljava/lang/Double";
      case 'B' -> "Ljava/lang/Byte";
      case 'C' -> "Ljava/lang/Character";
      case 'S' -> "Ljava/lang/Short";
      case 'Z' -> "Ljava/lang/Boolean";
      default -> throw new IllegalArgumentException("Unexpected non primitive descriptor " + primitiveDescriptor);
    };
  }

  /**
   * Interface used to generate fields.
   */
  @FunctionalInterface
  public interface FieldAdder {

    void generateField(ClassWriter cw, CodeFlow codeflow);
  }

  /**
   * Interface used to generate {@code clinit} static initializer blocks.
   */
  @FunctionalInterface
  public interface ClinitAdder {

    void generateCode(MethodVisitor mv, CodeFlow codeflow);
  }

}
